<?php
defined('BASEPATH') OR exit('No direct script access allowed');

/**
 * MY_Model Class
 * 
 * @version 1.0.0
 * @author Jason
 * @date 2017年4月26日 上午10:19:18
 * 
 * @version	1.1.0
 * @desc	更改名称使其更符合语义，完善注释
 * @author	Jason
 * @date	2017年6月16日 上午11:20:46
 */
class MY_Model extends CI_Model
{
	protected $table_mapper = [
			'master' => 'master',
			'slaves'   => 'master_slaves',
	];
	
	/**
	 * 允许写入（插入&更新）的字段数组
	 */
	protected $allowed_fields = [
			'master' => ['master_field'],
			'slaves' => ['slaves_field'],
	];
	
	public function __construct()
	{
		parent::__construct();
	}
	
	/**
	 * 从提交数据中提取允许写入（插入&更新）的数据数组
	 * 
	 * @param array  $post		待写入的数据超集，可以是表单提交，或者程序重组
	 * @param string $mapping	表映射，用来获取允许写入的字段集合。默认null代表跳过该参数，取下一获取方案。如果指定，则优先。
	 * @param mixed  $or_fields	直接指定允许写入的字段集。如果仍然获取不到字段集，原样返回！
	 * 
	 * @return array	过滤过的数组或原输入数组
	 */
	protected function get_allowed_write_data($post, $mapping = null, $or_fields = null)
	{
		$fields = $or_fields;
		if (!empty($this->allowed_fields) 
				&& !empty($this->allowed_fields[$mapping]) 
				&& is_array($this->allowed_fields[$mapping])) {
			$fields = $this->allowed_fields[$mapping];
		}
		if (!is_array($fields)) {
			return $post;
		}
		
		$data = [];
		foreach ($fields as $field) {
			if (isset($post[$field])) {
				$data[$field] = $post[$field];
			}
		}
		
		return $data;
	}
	
	/**
	 * 从提交数据中创建一个模型实例数据
	 * 
	 * @param array  $post		待写入的数据超集，可以是表单提交，或者程序重组
	 * @param string $mapping	表映射
	 * 
	 * @version	1.1.0
	 * @date	2017年6月20日 下午6:29:30
	 * @desc	将非数据库字段的过滤 挪 移 到 before_data_handler 之后，这样 before_data_handler 可以做更多的事情！
	 * 			比如从辅助字段中转变状态变量等，See To Role.before_create_data_handler。下个函数同。
	 * 
	 * @desc	修缮注释
	 * @date	2017年7月24日 下午1:49:05
	 * 
	 * @return	array	[状态，内容]
	 * 				状态为布尔值：代表成功或失败
	 * 				内容为数组，
	 * 					如果成功，内容为带 'id' 的数据
	 * 					如果失败，内容则为一个数组，三个元组：分别为（可以直接JSON渲染的errors & CI形式的 DB Error ($this->db->error()) & 欲插入的数据）
	 * 				内容 eg:
	 * 					array(
	 * 							'errors' => ["Error: ErrorMessage"], 
	 * 							'error' => array('code'=>1062, 'message'=>'Duplicate entry 'touroute-97-风景好赞一个啊' for key 'ukey_composite_unique''), 
	 * 							'data' => [...],
	 * 					)
	 */
	public function create_instance($post, $mapping = 'master')
	{
		if(method_exists($this, 'before_create_data_handler')) {
			$post = $this->before_create_data_handler($post, $mapping);
		}
		$data = $this->get_allowed_write_data($post, $mapping);
		$fields = $this->db->list_fields($this->table_mapper[$mapping]);
		if (in_array('create_time', $fields)) {
			$data['create_time'] = time();
		}
		if (in_array('member_id', $fields) && !empty($this->session->LOGINED_MEMBER['id'])) {
			$data['member_id'] = $this->session->LOGINED_MEMBER['id'];
		}
		if (in_array('status', $fields)) {
			isset($data['status']) or $data['status'] = 0;
		}
		
		$os = $this->db->insert($this->table_mapper[$mapping], $data);
		$affected_rows = $this->db->affected_rows();
		
		if ($affected_rows > 0) {
			$data['id'] = $this->db->insert_id();
			$status = true;
			$content = $data;
		} else if ($affected_rows == 0) {
			$status = false;
			$content = $data;
		} else {
			//var_dump($affected_rows);//-1
			$error = $this->db->error();
			# Has keys 'code' and 'message'
			#array(2) {
			#	'code' => int(0)
			#	'message' => string(0) ""
			#}
			#or
			#array(2) {
			#	'code' => int(1062)
			#	'message' => string(85) "Duplicate entry 'touroute-97-风景好赞一个啊' for key 'ukey_composite_unique'"
			#}
			$status = false;
			$content = [
					//'errors' => ["Error: {$error['code']}"],
					'errors' => ["Error: {$error['message']}"],
					'error' => $error,
					'data' => $data,
			];
		}
		
		// MARKDONE 这个应该改成返回两个值，因为 affected_rows > 0 未必 insert_id > 0，得看是否自增(AUTO_INCREMENT)
		return [$status, $content];
	}
	
	public function update_instance_by_id($id, $post, $mapping = 'master')
	{
		if(method_exists($this, 'before_update_data_handler')) {
			$post = $this->before_update_data_handler($id, $post, $mapping);
		}
		$data = $this->get_allowed_write_data($post, $mapping);
		$fields = $this->db->list_fields($this->table_mapper[$mapping]);
		if (in_array('update_time', $fields)) {
			$data['update_time'] = time();
		}
		
		$boolean = $this->db->update($this->table_mapper[$mapping], $data, ['id'=>$id]);
		
		if ($boolean) {
			$content = $data;
		} else {
			$error = $this->db->error();
			$content = [
					//'errors' => ["Error: {$error['code']}"],
					'errors' => ["Error: {$error['message']}"],
					'error' => $error,
					'data' => $data,
			];
		}
		
		// MARK $boolean 为真并不意味着 $this->db->affected_rows() 大于 0！- affected_rows：如果提交修改的数据和原数据完全相同，则为 0！！！
		//var_dump($boolean); var_dump($this->db->affected_rows());
		#return ($this->db->affected_rows() > 0);
		#return $boolean;
		return [$boolean, $content];
	}
	
	public function delete_instance_by_id($id, $mapping = 'master')
	{
		$status = true;
		if(method_exists($this, 'before_delete_data_handler')) {
			$status = $this->before_delete_data_handler($id, $mapping);
		}
		$affected_rows = 0;
		if ($status === true) {
			$this->db->delete($this->table_mapper[$mapping], ['id'=>$id]);
			$affected_rows = $this->db->affected_rows();
		}
		
		return [($affected_rows > 0), $status === true ? [] : [$status]];
	}
	
	/**
	 * 按条件删除多个模型实例数据
	 *
	 * @desc    description
	 * @author  Jason
	 * @date	2017年7月28日 下午7:46:47
	 *
	 * @param   array	$conditions
	 * @param   string	$mapping	表映射
	 *
	 * @return  array
	 */
	public function delete_instances_by_conditions($conditions, $mapping = 'master')
	{
		$this->db->from($this->table_mapper[$mapping]);
		$this->conditions_handler($conditions);
		return $this->db->delete();
	}
	
	/**
	 * 更新/替换指定外键的从表数据
	 * 
	 * @desc    更新/替换指定外键的从表数据
	 * @author  Jason
	 * @date    2017年6月7日 上午11:34:38
	 * 
	 * @param number $master_id
	 * @param string $slave_mapping     从表的表映射，无默认、勿缺省，必填！
	 * @param string $slave_foreign_key 从表的外键字段名，必填！
	 * @param array  $post              其中id可以为0或正数
	 * 
	 * @return  boolean
	 */
	public function replace_slaves_by_master_id($master_id, $slave_mapping, $slave_foreign_key, $post)
	{
		$table = $this->table_mapper[$slave_mapping];
		$this->db->from($table);
		$this->db->where($slave_foreign_key, $master_id);
		$orign_exists = $this->db->get()->result_array();
		$orign_exists = array_column($orign_exists, null, 'id');
		
		$post_inserts = [];
		$post_updates = [];
		$post_deletes = [];
		foreach ($post as $item) {
			if (empty($item['id'])) {
				$item[$slave_foreign_key] = $master_id;
				$post_inserts[] = $item;
				continue;
			}
			if (array_key_exists($item['id'], $orign_exists)) {
				$post_updates[$item['id']] = $item;
				continue;
			}
		}
		foreach ($orign_exists as $item) {
			if (!array_key_exists($item['id'], $post_updates)) {
				$post_deletes[$item['id']] = $item;
			}
		}
		
		foreach ($post_inserts as $item) {
			$this->db->insert($table, $item);
		}
		foreach ($post_updates as $item) {
			$this->db->update($table, $item, ['id', $item['id']]);
		}
		foreach ($post_deletes as $item) {
			$this->db->delete($table, ['id', $item['id']]);
		}
	}
	
	/**
	 * 按条件查询多个模型实例数据
	 * 
	 * @desc    description
	 * @author  Jason
	 * @date    2017年5月15日 上午10:22:37
	 * 
	 * @param   array	$conditions
	 * @param   string	$mapping	表映射
	 * 
	 * @return  array
	 */
	public function select_instances_by_conditions($conditions, $mapping = 'master')
	{
		$this->db->from($this->table_mapper[$mapping]);
		$this->conditions_handler($conditions);
// 		if ($this->table_mapper[$mapping] == 'language') {
// 			var_dump($this->db->get_compiled_select());die();
// 		}
		$query = $this->db->get();
		return $query->result_array();
	}
	
	/**
	 * 按条件（键）查询单一模型实例数据，这个条件应该是唯一索引键
	 *
	 * @desc    这个条件应该是唯一索引键，或者自己保证查询结果唯一，否则查询出的结果可能不稳定!
	 * @author  Jason
	 * @date	2017年7月12日 上午10:59:42
	 *
	 * @param   array	$conditions
	 * @param   string	$mapping	表映射
	 *
	 * @return  array
	 */
	public function select_instance_by_conditions($conditions, $mapping = 'master')
	{
		$this->db->from($this->table_mapper[$mapping]);
		$conditions[] = ['limit', 1];
		$this->conditions_handler($conditions);
		$query = $this->db->get();
		return $query->row_array();
	}
	
	/**
	 * 查询指定条件的模型实例数据的数量
	 *
	 * @desc    get_count_by_condition
	 * @author  Jason
	 * @date	2017年7月6日 下午4:38:09
	 *
	 * @param   array	$conditions
	 * @param   string	$mapping	表映射
	 *
	 * @return  array
	 */
	public function select_count_by_conditions($conditions, $mapping = 'master')
	{
		$this->db->from($this->table_mapper[$mapping]);
		$this->conditions_handler($conditions);
		$rowCount = $this->db->count_all_results();
		return $rowCount;
	}
	
	/**
	 * 根据主键ID查询一个模型实例数据
	 * 
	 * @desc    主键字段为 "id"
	 * @author  Jason
	 * @date    2017年6月2日 下午1:42:09
	 * 
	 * @param   number	$id	主键ID
	 * @param   string	$mapping	表映射
	 * 
	 * @return array
	 */
	public function select_instance_by_id($id, $mapping = 'master')
	{
		$table = $this->table_mapper[$mapping];
		$where = ['id' => $id];
		$limit = 1;
		return $this->db->get_where($table, $where, $limit)->row_array();
	}
	
	/**
	 * 从数据库中快速选择出某一个或某几个字段
	 * 
	 * @param number $id
	 * @param mixed  $fields	字段，类型字符串或字符串数组(string/array<string>)
	 * 
	 * @return mixed 类型与 $fields 对应
	 */
	public function select_fields_by_id($id, $fields, $mapping = 'master')
	{
		$this->db->select($fields);
		$this->db->from($this->table_mapper[$mapping]);
		$this->db->where('id', $id);
		$this->db->limit(1);
		
		$item = $this->db->get()->row_array();
		if (!empty($row)) {
			return [];
		}
		return is_string($fields) ? $item[$fields] : $item;
	}
	
	/**
	 * 获得分页的“五页”信息
	 * 
	 * @desc    description
	 * @author  Jason
	 * @date    2017年5月8日 上午11:39:34
	 * 
	 * @param number $currPage
	 * @param number $rowCount
	 * @param number $pageSize
	 * 
	 * @return  array	分页相关信息
	 */
	private static function paging($currPage, $rowCount, $pageSize = 10)
	{
		// 过滤p参数
		if (!intval($currPage)) {
			$currPage = intval($currPage);
		}
		// 总页数 = 总记录数 除以 每页显示数 进一取整
		$pageCount = ceil($rowCount / $pageSize);
		// 首页码
		$firstPage = 1; // or 0?
		// 尾页码
		$lastPage = $pageCount;
		// 当前页码
		$currPage = max($currPage, $firstPage);
		// 上一页码
		$prevPage = max($currPage - 1, $firstPage);
		// 下一页码
		$nextPage = min($currPage + 1, $lastPage);
		
		$pageInfo = [
				'page' => $currPage,	// 当前页码
				'size' => $pageSize,	// 页面大小；每页显示数量
				'count' => $pageCount,	// 总页数；最后一页页码
// 				'first' => $firstPage,	// 第一页页码
				'prev' => $prevPage,	// 上一页页码
				'next' => $nextPage,	// 下一页页码
				'last' => $lastPage,	// 最后一页页码
				'total' => $rowCount,	// 数据总数
		];
		return $pageInfo;
	}
	
	/**
	 * 获得多条记录集,有分页
	 * 
	 * @desc    description
	 * @author  Jason
	 * @date    2017年5月8日 下午2:03:52
	 *
	 * @param array  $conditions 查询条件    -> 示例参考 conditions_handler
	 * @param int    $page       当前分页
	 * @param int    $number     每页的数据量
	 * @param string $mapping    表映射
	 *
	 * @return array ['data'=>[], 'pages'=>[]]
	 */
	public function get_list($conditions, $page, $number, $mapping = 'master')
	{
		$page = intval($page) > 1 ? intval($page) : 1;
		$limit = $number;
		$offset = ($page - 1) * $number;
		
		$table = $this->table_mapper[$mapping];
		$this->db->from($table);
		
		// 自定义 condition 处理（提取出来！）
		$this->conditions_handler($conditions);
		
		$rowCount = $this->db->count_all_results('', false);
		$pages = self::paging($page, $rowCount, $number);
		
		$this->db->limit($limit, $offset);
		$data = $this->db->get()->result_array();
		
		return ['data' => $data, 'pages'=>$pages];
	}
	
	/**
	 * 自定义 condition 处理
	 * 
	 * @desc    自定义 condition 处理
	 * @author  Jason
	 * @date    2017年5月12日 下午6:41:24
	 * 
	 * @date	2017年5月15日 上午10:47:14
	 * @desc	重构，全面支持传递各个函数的全部参数
	 * 
	 * @date	2017年5月17日 下午6:04:46
	 * @desc	Fix: where 与 where_in 系列虽然参数表列相同，但首二参数的类型却不同，传参的灵活性也不一致！
	 * 
	 * @param   array	$conditions 二维数组，每个子数组也是一个数组，下标"0"表示方法名，下标"1"表示参数数组，或首参数字符串
	 * 			eg: array(
	 * 					['select', '*'],
	 * 					['select', ['id, name']],
	 * 					['select', ['`id`, `name`, `status`&8>>3 as enabled', FALSE]],
	 * 					['distinct'],
	 * 					['distinct', true],
	 * 					['distinct', [true]],
	 * 					['where', 'where_clause'],
	 * 					['where', [where_function_params]],
	 * 					['where', ['id'=>'1', 'status'=>'15']],
	 * 					// ['where', [['id'=>'1', '`status`&8>>3'=>'1'], null, false]], // find a BUG
	 * 					['where', [['id'=>'1', '`status`&8>>3='=>'1'], null, false]], // fix fill
	 * 					...
	 * 					#['where_in', [['resource' => $resources]]], // Bad!
	 * 					['where_in', ['resource', $resources]], // OK
	 * 					...
	 * 			)
	 * 
	 * @return  boolean	是否有处理不了的条件项
	 */
	protected function conditions_handler($conditions, $db = null)
	{
		// $db = $db ? $db : $this->db;
		$allok = true;
		foreach ($conditions as $uselessIndex => $condition) {
			@list($method, $params) = $condition;
			if (empty($params)) {
				$params = [];
			} else if (!is_array($params)) {
				$params = array($params);
			}
			switch ($method) {
				case 'select':
					// ([$select = '*'[, $escape_is_protected_identifier = NULL]])
				case 'join':
					// ($table, $cond[, $type = ''[, $escape = NULL]])
				case 'where':
					// ($key[, $value = NULL[, $escape = NULL]])
				case 'or_where':
				case 'where_in':
				case 'where_not_in':
				case 'or_where_in':
				case 'or_where_not_in':
					// ($key[, $value = NULL[, $escape = NULL]])
					// [or_]where:          $key (mixed) -- Name of field to compare, or associative array
					// [or_]where_[not_]in: $key (string) -- Name of field to examine
				case 'like':
				case 'or_like':
				case 'not_like':
				case 'or_not_like':
					// ($field[, $match = ''[, $side = 'both'[, $escape = NULL]]])
				case 'group_by':
					// ($by[, $escape = NULL])
				case 'distinct':
					// ([$val = TRUE])
				case 'having':
				case 'or_having':
					// ($key[, $value = NULL[, $escape = NULL]])
				case 'order_by':
					// ($orderby[, $direction = ''[, $escape = NULL]])
				case 'limit':
					// ($value[, $offset = 0])
				case 'offset':
					// ($offset)
				case 'group_start':
				case 'or_group_start':
				case 'not_group_start':
				case 'or_not_group_start':
				case 'group_end':
					// (), 查询条件组, 无参数
					$callback = [$this->db, $method];
					$param_arr = $params;
					call_user_func_array($callback, $param_arr);
					break;
				default:
					log_message('error', sprintf("The Index '%s' of Condition '%s' is NOT supported!", $method, var_export($conditions, true)));
					$allok = false;
			}
		}
		
		return $allok;
	}
// 	protected function conditions_handler($conditions, $db = null)
// 	{
// 		// $db = $db ? $db : $this->db;
// 		$allok = true;
// 		foreach ($conditions as $method => $params) {
// 			switch ($method) {
// 				case 'select':
// 					$this->db->select($params);
// 					break;
// 				case 'where':
// 				case 'or_where':
// 					$this->db->$method($params);
// 					break;
// 				case 'where_in':
// 				case 'where_not_in':
// 				case 'or_where_in':
// 				case 'or_where_not_in':
// 					$this->db->$method($params[0], $params[1]);
// 					break;
// 				case 'like':
// 				case 'or_like':
// 				case 'not_like':
// 				case 'or_not_like':
// 					$this->db->$method($params[0], null, $params[1]);
// 					break;
// 				case 'group_by':
// 					$this->db->group_by($params);
// 					break;
// 				case 'distinct':
// 					$this->db->distinct();
// 					break;
// 				case 'having':
// 				case 'or_having':
// 					$this->db->$method($params);
// 					break;
// 				case 'order_by':
// 					$this->db->order_by($params);
// 					break;
// 				default:
// 					log_message('error', sprintf("The Index '%s' of Condition '%s' is NOT supported!", $method, var_export($conditions, true)));
// 					$allok = false;
// 			}
// 		}
		
// 		return $allok;
// 	}
//	
// 	protected function conditions_metrix_handler($conditions_metrix, $db = null)
// 	{
// 		// $db = $db ? $db : $this->db;
// 		$allok = true;
// 		foreach ($conditions_metrix as $uselessIndex => $conditions) {
// 			if (!$this->conditions_handler($conditions, $db)) {
// 				$allok = false;
// 			}
// 		}
		
// 		return $allok;
// 	}
}